from stat import * 
import os
import sys
import unittest
import infinitest

def _flatten(L):
    """flatten an array with one level of depth only"""
    new_list = []
    for x in L:
        new_list.extend(x)
    return new_list

def _extractLineNumberAndFilename(errors):
    for error_tuple in errors:
        error_tuple = errors[0]
        exception = error_tuple[1]
        if not hasattr(exception, 'filename'):
            # Todo: what if we don't get a line & file? 
            continue
        return os.path.split(exception.filename)[1] + ":" + str(exception.lineno)

def _extractFilenamesFromResults(results):
    return [_extractLineNumberAndFilename(r.errors or r.failures) 
            for r in results 
            if not r.wasSuccessful()]

def noisySubscriber(results):
    """ This is really not very useful except for debugging purposes"""
    print "--------------\n\nCompleted new tests"
    (total,failed,errors) = (0,0,0)
    allErrors = []
    allFailed = []
    for result in results:
        total += result.testsRun
        failed += len(result.failures)
        errors += len(result.errors)
        allFailed.extend(result.failures)
        allErrors.extend(result.errors)
    good = total - len(allFailed) - len(allErrors)
    print "Good: %d, Bad: %d, Ugly: %d" % (good, failed, errors)
    from pprint import pprint
    if len(allErrors) > 0:
        print "ERRORS", "-"*60
        pprint(allErrors)
    if len(allFailed) > 0:
        print "FAILURES", "-"*60
        pprint(allFailed)

class InfinitestTest(unittest.TestCase):
    PROJECT_HOME = "testProject"
    TEMP_FILE = PROJECT_HOME + "/FakeProduct.py.temp"
    ALT_FILE = PROJECT_HOME + "/FakeProduct.py.alt"
    ORIG_FILE = PROJECT_HOME + "/FakeProduct.py"

    def test_pathFromUserInput(self):
        path = os.getcwd()
        self.failUnlessEqual(path, infinitest.pathFromUserInput(path))
        self.failUnlessEqual(path, infinitest.pathFromUserInput("."))
        self.failUnlessEqual(path, infinitest.pathFromUserInput("././././"))

        path = os.path.expanduser("~")
        self.failUnlessEqual(path, infinitest.pathFromUserInput("~"))
        
    def setUp(self):
        self.infinitest = infinitest.InfinitestCore(self.PROJECT_HOME)
        # self.infinitest.subscribe(noisySubscriber)
        self.resetFileTest()

    def tearDown(self):
        self.resetFileTest()
        if os.path.exists(self.TEMP_FILE):
            os.rename(self.ORIG_FILE, self.ALT_FILE)
            os.rename(self.TEMP_FILE, self.ORIG_FILE)

    FLAG_FILE = "fileCreatedByTest.temp"
    def resetFileTest(self):
        if os.path.exists(self.FLAG_FILE):
            os.remove(self.FLAG_FILE)
    def fileTestWasRun(self):
        return os.path.exists(self.FLAG_FILE)

    def testConstructorSetsProjectDir(self):
        self.failUnlessEqual(self.infinitest.dir, self.PROJECT_HOME)

    def testHandlesInvalidSyntaxWell(self):
        results = self.infinitest.runTests()
        failing_test_names = _extractFilenamesFromResults(self.infinitest.runTests())
        self.failUnless("notValidSyntax.py:16" in failing_test_names )

    def testReloadsTests(self):        
        results = self.infinitest.runTests()
        failing_test_names = _extractFilenamesFromResults(results)
        self.failIf("FakeProduct.py:24" in failing_test_names )
        os.rename(self.ORIG_FILE, self.TEMP_FILE)
        os.rename(self.ALT_FILE, self.ORIG_FILE)
        failing_test_names = _extractFilenamesFromResults(self.infinitest.runTests())
        self.failUnless("FakeProduct.py:24" in failing_test_names )

    def testRunsTests(self):  
        results = self.infinitest.runTests()
        self.failUnless(self.fileTestWasRun())
        self.failUnlessEqual(8, len(results))

    def testDetectsChanges(self):
        sut = self.infinitest
        self.failUnless(sut.filesHaveChanged())
        self.failIf(sut.filesHaveChanged())
        self.touch(os.path.join(self.PROJECT_HOME, "FakeProduct.py"))
        self.failUnless(sut.filesHaveChanged())
        self.failIf(sut.filesHaveChanged())

    def touch(self, file):
        mod_time = os.stat(file)[ST_MTIME]
        mod_time += 100
        os.utime(file, (mod_time, mod_time))

    def testPythonFilenameToModuleName(self):
        path,actual = self.infinitest.relativeModulePathAndFile("etc/water/bug.py")
        self.failUnlessEqual("etc.water.bug", actual)

    def testGetModuleFromTestNameShouldBeRelativeToProjectDir(self):
        module = self.infinitest.getModuleForTest("testProject/__init__.py")
        self.failUnlessEqual("__init__", module.__name__)        
        module = self.infinitest.getModuleForTest("testProject/subFolder/__init__.py")
        self.failUnlessEqual("subFolder.__init__", module.__name__)        

    def testUpdate(self):
        self.infinitest.update()
        self.failUnless(self.fileTestWasRun())

        self.resetFileTest()
        self.infinitest.update()
        self.failIf(self.fileTestWasRun())

        self.resetFileTest()
        self.touch(self.PROJECT_HOME + os.path.sep + "FakeProduct.py")
        self.infinitest.update()
        self.failUnless(self.fileTestWasRun())

    def testFindsFiles(self):
        tests = self.infinitest._findFiles()
        self.failUnless(len(tests) >= 3)
        baseDir = self.PROJECT_HOME + os.path.sep
        self.failUnlessEqual(1, tests.count(baseDir + "FakeProduct.py"), tests)
        self.failUnlessEqual(1, tests.count(baseDir + "subFolder/AnotherFake.py"), tests)
        self.failUnlessEqual(1, tests.count(baseDir + "subFolder/superSub/YetAnotherFake.py"), tests)

    def testFindsTestFiles(self):
        def stubbedFindFiles():
            return [ "python.py",
                    "detestable.py",
                    "testSomething.py",
                    "blugTest.py",
                    "Test_nonsense.py",
                    "nonsense_TEST.py",
                    "ignoretestinmiddle.py",
                    "imptest.txt",
                    "howToTest.doc"
                    ]
        self.infinitest._findFiles = stubbedFindFiles
        actual = self.infinitest._findTestFiles()
        self.failUnless("testSomething.py" in actual)
        self.failUnless("blugTest.py" in actual)
        self.failUnless("Test_nonsense.py" in actual)
        self.failUnless("nonsense_TEST.py" in actual)
        self.failIf("python.py" in actual)
        self.failIf("detestable.py" in actual)
        self.failIf("ignoretestinmiddle.py" in actual)
        self.failIf("imptest.txt" in actual)
        self.failIf("howToTest.doc" in actual)

    def testIgnoresTXTFile(self):
        nontest = os.path.join(self.PROJECT_HOME, "NotATest.txt")
        tests = self.infinitest._findFiles()
        self.failUnless(os.path.isfile(nontest))
        self.failUnlessEqual(0, tests.count(nontest), tests)

class InfinitestTestSubscription(unittest.TestCase):
    def setUp(self):
        self.infinitest = infinitest.InfinitestCore(".")

    def testPublishWhenFilesHaveChanged(self):
        def callMe(results):
            callMe.calls += 1
        callMe.calls = 0
        self.infinitest.filesHaveChanged = lambda: True
        self.infinitest.runTests = lambda: True

        self.infinitest.subscribe(callMe)

        self.infinitest.update()
        self.failUnlessEqual(1, callMe.calls)

    def testNoPublishUnlessTestsWereRun(self):
        sut = self.infinitest
        sut.filesHaveChanged =  lambda: False
        calls = 0
        def incrementCalls(results):
            calls += 1
        sut.runTests = incrementCalls
        sut.update()
        self.failUnlessEqual(0, calls)

def fakeTimer(*times):
    times = list(times)
    def inner_fakeTimer():
        """Returns a preconfigured set of times: %s """ % repr(times)
        entry = times[0]
        times.pop(0)
        return entry
    return inner_fakeTimer

def fakeSleeper(time):
    pass

class RecurringTimerTest(unittest.TestCase):
    def setUp(self):
        self.callcount = 0
    def callbackTest(self):
        self.callcount += 1

    def buildFakeTimer(self, cycles, cycle_time):
        timer = infinitest.RecurringTimer(self.callbackTest, cycle_time_in_ms=cycle_time, num_cycles=cycles)
        timer.sleep_for_ms = fakeSleeper
        return timer

    def testSingleCycleOfTimer(self):
        timer = self.buildFakeTimer(cycles=1, cycle_time=10)
        timer.currentTimeInMs = fakeTimer(0,101)
        timer.start()
        self.failUnlessEqual(1, self.callcount)

    def testTimerCallsTwiceIfSleeptimeIsLessThanOrEqualToCycleTime(self):
        timer = self.buildFakeTimer(cycles=2, cycle_time=100)
        timer.currentTimeInMs = fakeTimer(0,100, 100,140)
        timer.start()
        self.failUnlessEqual(2, self.callcount)

    def testTimerSleepsReasonableAmounts(self):
        timer = self.buildFakeTimer(cycles=1, cycle_time=100)
        timer.currentTimeInMs = fakeTimer(0,90)
        def sleep_spy(x):
            sleep_spy.last  = x
        timer.sleep_for_ms = sleep_spy

        timer.start()
        self.failUnlessEqual(10, sleep_spy.last)


if __name__ == '__main__':
    unittest.main()
